"""
01_connection_basics.py
=======================
Neo4j Connection Fundamentals

This module covers:
1. Connecting to Neo4j with Python driver
2. Connection pooling and configuration
3. Session management
4. Error handling
5. Health checks

Author: Neo4j Tutorial
Date: January 2025
"""

import os
from typing import Dict, List, Any, Optional
from contextlib import contextmanager
from dataclasses import dataclass
from dotenv import load_dotenv

# Load environment variables
load_dotenv()

# Neo4j Python Driver
try:
    from neo4j import GraphDatabase, Driver, Session
    from neo4j.exceptions import (
        ServiceUnavailable, 
        AuthError, 
        ClientError,
        TransientError
    )
    NEO4J_AVAILABLE = True
except ImportError:
    NEO4J_AVAILABLE = False
    print("Neo4j driver not installed. Run: pip install neo4j")


@dataclass
class Neo4jConfig:
    """Configuration for Neo4j connection."""
    uri: str = "bolt://localhost:7687"
    username: str = "neo4j"
    password: str = "password123"
    database: str = "neo4j"
    max_connection_pool_size: int = 50
    connection_timeout: float = 30.0
    max_transaction_retry_time: float = 30.0


class Neo4jConnection:
    """
    Neo4j Connection Manager with pooling support.
    
    This class provides:
    - Connection pooling for efficient resource usage
    - Automatic reconnection on failures
    - Session management with context managers
    - Health check capabilities
    
    Usage:
        # Method 1: Direct instantiation
        conn = Neo4jConnection()
        conn.connect()
        result = conn.execute_query("MATCH (n) RETURN count(n)")
        conn.close()
        
        # Method 2: Context manager
        with Neo4jConnection() as conn:
            result = conn.execute_query("MATCH (n) RETURN count(n)")
        
        # Method 3: Using get_connection singleton
        conn = get_connection()
        result = conn.execute_query("MATCH (n) RETURN count(n)")
    """
    
    _instance = None  # Singleton instance
    
    def __init__(self, config: Neo4jConfig = None):
        """
        Initialize connection manager.
        
        Args:
            config: Neo4jConfig object or None for defaults/env vars
        """
        self.config = config or Neo4jConfig(
            uri=os.getenv("NEO4J_URI", "bolt://localhost:7687"),
            username=os.getenv("NEO4J_USERNAME", "neo4j"),
            password=os.getenv("NEO4J_PASSWORD", "password123"),
            database=os.getenv("NEO4J_DATABASE", "neo4j")
        )
        self._driver: Optional[Driver] = None
    
    def connect(self) -> 'Neo4jConnection':
        """
        Establish connection to Neo4j.
        
        Returns:
            Self for method chaining
            
        Raises:
            AuthError: If authentication fails
            ServiceUnavailable: If server is not reachable
        """
        if not NEO4J_AVAILABLE:
            raise ImportError("neo4j package not installed")
        
        if self._driver is None:
            self._driver = GraphDatabase.driver(
                self.config.uri,
                auth=(self.config.username, self.config.password),
                max_connection_pool_size=self.config.max_connection_pool_size,
                connection_timeout=self.config.connection_timeout,
                max_transaction_retry_time=self.config.max_transaction_retry_time
            )
        return self
    
    def close(self):
        """Close the connection and release resources."""
        if self._driver:
            self._driver.close()
            self._driver = None
    
    @property
    def driver(self) -> Driver:
        """Get the underlying driver, connecting if necessary."""
        if self._driver is None:
            self.connect()
        return self._driver
    
    def __enter__(self) -> 'Neo4jConnection':
        """Context manager entry."""
        return self.connect()
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.close()
    
    # =========================================================
    # SESSION MANAGEMENT
    # =========================================================
    
    @contextmanager
    def session(self, database: str = None):
        """
        Get a session with context management.
        
        Args:
            database: Database name (optional, uses config default)
            
        Yields:
            Neo4j Session object
        """
        session = self.driver.session(
            database=database or self.config.database
        )
        try:
            yield session
        finally:
            session.close()
    
    def get_session(self, database: str = None) -> Session:
        """
        Get a raw session (caller must close).
        
        Args:
            database: Database name
            
        Returns:
            Neo4j Session
        """
        return self.driver.session(
            database=database or self.config.database
        )
    
    # =========================================================
    # QUERY EXECUTION
    # =========================================================
    
    def execute_query(
        self,
        query: str,
        parameters: Dict[str, Any] = None,
        database: str = None
    ) -> List[Dict[str, Any]]:
        """
        Execute a Cypher query and return results.
        
        Args:
            query: Cypher query string
            parameters: Query parameters (optional)
            database: Database name (optional)
            
        Returns:
            List of result records as dictionaries
            
        Example:
            results = conn.execute_query(
                "MATCH (p:Patient {id: $id}) RETURN p",
                {"id": "P001"}
            )
        """
        with self.session(database) as session:
            result = session.run(query, parameters or {})
            return [record.data() for record in result]
    
    def execute_write(
        self,
        query: str,
        parameters: Dict[str, Any] = None,
        database: str = None
    ) -> Any:
        """
        Execute a write transaction.
        
        Args:
            query: Cypher query string
            parameters: Query parameters
            database: Database name
            
        Returns:
            Result summary
        """
        def _write_tx(tx):
            result = tx.run(query, parameters or {})
            return result.consume()
        
        with self.session(database) as session:
            return session.execute_write(_write_tx)
    
    def execute_read(
        self,
        query: str,
        parameters: Dict[str, Any] = None,
        database: str = None
    ) -> List[Dict[str, Any]]:
        """
        Execute a read transaction.
        
        Args:
            query: Cypher query string
            parameters: Query parameters
            database: Database name
            
        Returns:
            List of result records
        """
        def _read_tx(tx):
            result = tx.run(query, parameters or {})
            return [record.data() for record in result]
        
        with self.session(database) as session:
            return session.execute_read(_read_tx)
    
    # =========================================================
    # HEALTH CHECK
    # =========================================================
    
    def health_check(self) -> Dict[str, Any]:
        """
        Check database health and connectivity.
        
        Returns:
            Health status dictionary
        """
        try:
            self.driver.verify_connectivity()
            
            # Get server info
            with self.session() as session:
                result = session.run("CALL dbms.components()")
                components = [r.data() for r in result]
                
                result = session.run("MATCH (n) RETURN count(n) as nodeCount")
                node_count = result.single()["nodeCount"]
            
            return {
                "status": "healthy",
                "components": components,
                "node_count": node_count,
                "uri": self.config.uri
            }
        except ServiceUnavailable as e:
            return {"status": "unavailable", "error": str(e)}
        except AuthError as e:
            return {"status": "auth_error", "error": str(e)}
        except Exception as e:
            return {"status": "error", "error": str(e)}
    
    def get_database_info(self) -> Dict[str, Any]:
        """Get database metadata and statistics."""
        query = """
        CALL dbms.components() YIELD name, versions, edition
        RETURN name, versions, edition
        """
        return self.execute_query(query)


# ============================================================
# SINGLETON ACCESS
# ============================================================

_connection_instance: Optional[Neo4jConnection] = None


def get_connection() -> Neo4jConnection:
    """
    Get singleton connection instance.
    
    Returns:
        Shared Neo4jConnection instance
    """
    global _connection_instance
    if _connection_instance is None:
        _connection_instance = Neo4jConnection()
        _connection_instance.connect()
    return _connection_instance


def get_driver() -> Driver:
    """
    Get the Neo4j driver from singleton connection.
    
    Returns:
        Neo4j Driver instance
    """
    return get_connection().driver


# ============================================================
# DEMONSTRATION
# ============================================================

if __name__ == "__main__":
    print("=" * 60)
    print("Neo4j Connection Basics - Demonstration")
    print("=" * 60)
    
    # Method 1: Direct connection
    print("\n📌 Method 1: Direct Connection")
    print("-" * 40)
    try:
        conn = Neo4jConnection()
        conn.connect()
        
        # Health check
        health = conn.health_check()
        print(f"   Status: {health['status']}")
        if health['status'] == 'healthy':
            print(f"   Node Count: {health['node_count']}")
        
        conn.close()
        print("   Connection closed successfully")
    except Exception as e:
        print(f"   Error: {e}")
    
    # Method 2: Context manager
    print("\n📌 Method 2: Context Manager")
    print("-" * 40)
    try:
        with Neo4jConnection() as conn:
            result = conn.execute_query("RETURN 'Hello Neo4j!' as message")
            print(f"   Result: {result[0]['message']}")
    except Exception as e:
        print(f"   Error: {e}")
    
    # Method 3: Singleton
    print("\n📌 Method 3: Singleton Pattern")
    print("-" * 40)
    try:
        conn = get_connection()
        driver = get_driver()
        print(f"   Driver type: {type(driver).__name__}")
        print(f"   Connection reused: True")
    except Exception as e:
        print(f"   Error: {e}")
    
    print("\n" + "=" * 60)
    print("✅ Connection demonstration complete!")
    print("=" * 60)
